{% extends 'base.html' %}
{% block body %}
    {% if milestones.count %}
        <script src="/data/milestones/" type="text/javascript" charset="utf-8"></script>
        <script type="text/javascript+protovis">
            console.time('rendering');
            var ONE_DAY_MS=1000*60*60*24;
            
            function parseTimestamp(ts){
                var date_parts = ts.split('-');
                var day_time = date_parts[2].split(' ');
                var day = day_time[0];
                var time_parts = day_time[1].split(':');
                return new Date(
                    parseInt(date_parts[0]),
                    parseInt(date_parts[1]) - 1,
                    parseInt(day),
                    parseInt(time_parts[0]),
                    parseInt(time_parts[1]),
                    parseInt(time_parts[2])
                );
            }
            
            function overlap(rangeA, rangeB){
                return rangeA.start <= rangeB.end && rangeB.start <= rangeA.end;
            }
            
            var holidays = holidays.map(function(d){
                d.start = parseTimestamp(d.start);
                d.end = parseTimestamp(d.end);
                d.tooltipUrl = "/calendar/day/?day="+d.start.format('d')+"&month="+ d.start.format('m') +"&year=" + d.start.format('Y');
                return d;
            });
            
            var obligations = obligations.map(function(d){
                d.start = parseTimestamp(d.start);
                d.end = parseTimestamp(d.end);
                var hours = (d.end - d.start) / ONE_DAY_MS * 24;
                if(hours < 9){ // Probably not a full day event - sloppy "hueristic"
                    var hours = (d.end - d.start) / ONE_DAY_MS * 24;
                    hours = (24 / (8 / hours)) - hours;
                    d.end.addHours(hours);
                }
                return d;
            });

            // Convert javascript date properties
            var milestones = milestones.map(function(d){
                d.due = new Date(d.due);
                return d;
            });

            // Sort by date ascending
            var milestones = milestones.sort(function(a, b){
                return a.due - b.due;
            });

            // Get milestone date stats
            var latest_milestone = milestones[milestones.length - 1].due;
            var earliest_milestone = milestones[0].due;
            var today = new Date();


            start = new Date(earliest_milestone);
            start.setDate(earliest_milestone.getDate() - 14);

            // Calculate work track length for each committer
            for(var alias in committers){
                committers[alias]['work_length'] = pv.sum(all_tickets.map(
                    function(d) !d.completed && d.owner === 
                        alias ? d.estimated_time : 0));
            }

            // Grab the longest work track to help calculate length of timeline
            var longest_work_track = pv.max(
                pv.entries(committers).map(function(d) d.value.work_length));

            var end = new Date();
            end.setDate(
                // how many days to finish the longest work track
                today.getDate() + (longest_work_track / 8) +
                // plus weekends every 5 days
                (((longest_work_track/8) / 5) * 2)
                // not yet considering holidays and vacations
                
                + 26 // days padding
            );
            
            // if(end < latest_milestone){
            //     end.setDate(latest_milestone.getDate());
            // }
            
            end = latest_milestone;


            // options used when instanciating qtip tooltips
            var qtipOpts = {
                style: { 
                    tip: {
                        corner: 'topLeft',
                        color: '#6699CC',
                        size: {
                            x: 30,
                            y : 12
                        }
                    }
                }, 
                hide: {
                    delay:250,
                    fixed: true
                }
            }

            // Colors to apply to bars representing unfinished tickets
            var INCOMPLETE_COLORS = pv.colors("#87B6E9", "#6E9CB9");

            var num_days = Math.round(Math.abs(start.getTime() - end.getTime()) / ONE_DAY_MS);
            var day_ticks = pv.range(0, num_days, 1).map(function(d){
                var date = new Date(start);
                date.setDate(start.getDate() + d); 
                return date;
            });
            var weekends = [];
            for(var i=0;i<day_ticks.length;i++){
                var day = day_ticks[i];
                if(day.getDay() === 6){
                    var s = new Date(day);
                    var e = new Date(day);
                    e.addDays(2);
                    weekends.push({
                        weekend: true,
                        start: s,
                        end: e
                    });
                }
            }

            var w = day_ticks.length * 25;
            var x = pv.Scale.linear(start, end).range(0, w);

            /** 
            Ordinal scale for milestone/contributor horizontal tracks
            ie [
                'MarineMap:Milestone-1.0', 
                'MarineMap:Milestone-1.0-underbluewaters', 
                ...
                ]
            Adds milestone entry without contributor for labelling
            */
            var y_domain = [];
            var added = {};
            for(var i = 0; i<milestones.length; i++){
                var m = milestones[i];
                y_domain.push(m.project + ':' + m.name);
                for(var j=0;j<all_tickets.length;j++){
                    var t = all_tickets[j];
                    if(t.project === m.project && 'Milestone-'+t.milestone === m.name){
                        var label = t.project + ':Milestone-' + t.milestone + '-' + t.owner;
                        if(!added[label]){
                            added[label] = true;
                            y_domain.push(label);
                        }                    
                    }
                }
                // pv.uniq(all_tickets.map(function(d) d.project + ':Milestone-' d.milestone + '-' + d.owner));
            }
            // y_domain = pv.blend(milestones.map(function(m){
            //     return [m.project + m.name, m.project + ":" + m.name].concat(
            //         pv.uniq(
            //             all_tickets.map(
            //                 function(t) t.project + ':Milestone-' + t.milestone + '-' + t.owner)));
            // }));

            var row_height = 30;
            var h = 100 + (y_domain.length * row_height);

            var y = pv.Scale.ordinal(y_domain).splitBanded(70, h, 4/5);

            all_tickets.sort(function(a, b){
                return y(a.project + ':Milestone-' + a.milestone + '-' + a.owner) - 
                    y(b.project + ':Milestone-' + b.milestone + '-' + b.owner);
            });

            var weekend_masks = [];

            var events = weekends.concat(holidays).concat(obligations).sort(function(a, b){
                return a.start - b.start;
            });

            function add_intersecting_events(d, added){
                if(!added){
                    var added = [];
                }
                var intersects = false;
                rangeB = {start: d.start, end: d.end};
                for(var i=0;i<events.length;i++){
                    var event = events[i];
                    var rangeA = events[i];
                    if(added.indexOf(rangeA) === -1  && (!events[i]['committers'] || events[i].committers.indexOf(d.owner) != -1) && overlap(rangeA, rangeB)){
                        intersects = true;
                        // loop through added times and wittle down range to be added where there is overlap
                        // possibilities:
                        //      begining of event overlaps already added event, so rangeA.start should be advanced
                        //      event is wholly contained within already added event, so rangeA.end - rangeA.start should = -1
                        //      can't have situation where event only overlaps begining of already added event since events are sorted by start date
                        var rangeA = {start: event.start, end: event.end};
                        var addedHours = ((rangeA.end - rangeA.start) / ONE_DAY_MS) * 24;
                        for(var j=0;j<added.length;j++){
                            var addedEvent = added[j];
                            if(overlap(rangeA, addedEvent)){
                                // there is overlap
                                if(rangeA.start >= addedEvent.start && rangeA.end <= addedEvent.end){
                                    // wholly overlapping
                                    addedHours = 0;
                                }else{
                                    // New event's start overlaps added event. End is non-overlapping
                                    addedHours = addedHours - (((addedEvent.end - rangeA.start) / ONE_DAY_MS) * 24);
                                }
                            }
                        }
                        rangeB.end.addHours(addedHours);
                        // keep track of already intersected ranges
                        added.push(events[i]);
                        if(rangeA.weekend){
                            weekend_masks.push({
                                range: rangeA,
                                top: true,
                                y: y(rangeB.project + ':Milestone-' + rangeB.milestone + '-' + rangeB.owner) - 5
                            });
                            weekend_masks.push({
                                range: rangeA,
                                top: false,
                                y: y(rangeB.project + ':Milestone-' + rangeB.milestone + '-' + rangeB.owner) - 5
                            });
                        }
                        break;
                    }
                }
                if(intersects){
                    add_intersecting_events(d, added);
                }
            };

            // If no time estimate, set the length to 8  hours
            // Calculate start and end times for tickets
            var contributor_offsets = {};
            var first_tracks = {};
            var tracks = {};
            
            var all_tickets = all_tickets.map(function(d){
                d.tooltipUrl = "/tickets/tooltip/?ticket=" + d.id;
                if(d.updated){
                    var date_parts = d.updated.split('-');
                    var day_time = date_parts[2].split(' ');
                    var day = day_time[0];
                    var time_parts = day_time[1].split(':');
                    d.updated = new Date(
                        parseInt(date_parts[0]),
                        parseInt(date_parts[1]) - 1,
                        parseInt(day),
                        parseInt(time_parts[0]),
                        parseInt(time_parts[1]),
                        parseInt(time_parts[2])
                    );
                }
                if(d.owner === 'None'){
                    d.owner = 'Unassigned';
                }
                if(d.estimated_time && d.estimated_time !== 'None'){
                    d.estimated_time = parseInt(d.estimated_time);
                }else{
                    d.estimated_time = 8;
                }
                
                if(!contributor_offsets[d.owner]){
                    contributor_offsets[d.owner] = new Date();
                }
                if(!d.completed){
                    d.start = new Date(contributor_offsets[d.owner]);
                    d.end = new Date(d.start);
                    var hours_added = (d.estimated_time / 8.0) * 24;
                    hours_added = hours_added - (hours_added * (d.percent_complete / 100));
                    d.end.addHours(hours_added);
                    add_intersecting_events(d);
                    contributor_offsets[d.owner] = d.end;
                    if(!first_tracks[d.project + ':Milestone-' + d.milestone + '-' + d.owner]){
                        first_tracks[d.project + ':Milestone-' + d.milestone + '-' + d.owner] = true;
                        d.first = true;
                    }
                    if(!tracks[d.project + ':Milestone-' + d.milestone + '-' + d.owner]){
                        tracks[d.project + ':Milestone-' + d.milestone + '-' + d.owner] = {y: d.project + ':Milestone-' + d.milestone + '-' + d.owner};
                    }
                    track = tracks[d.project + ':Milestone-' + d.milestone + '-' + d.owner];
                    if(d.first){
                        track.first = d;
                    }
                    track.last = d;
                }
                return d;
            });
            
            var event_masks = [];
            for(var i=0;i<events.length;i++){
                var e = events[i];
                for(var key in tracks){
                    var track = tracks[key];
                    if(!e['committers']){
                        var include = true;
                    }else{
                        var include = false;
                        for(var j =0; j<e.committers.length;j++){
                            var alias = e.committers[j];
                            if(key.indexOf(alias) != -1){
                                include = true;
                                break;
                            }
                        }
                    }
                    if(include){
                        var range = {
                            start: track.first.start,
                            end: track.last.end
                        };
                        if(overlap(range, e)){
                            event_masks.push($.extend({y: key, type: e.type}, e));
                        }
                    }
                }
            }

            // Event row stripes
            var merged_masks = event_masks.map(function(d){
                return jQuery.extend({}, d)
            });
            
            function merge(list, index){
                var a = list[index];
                var merged = [];
                var has_merged = false;
                for(var i=index+1;i<list.length;i++){
                    var b = list[i];
                    if(a.y == b.y && overlap(a, b)){
                        has_merged = true;
                        if(b.end <= a.end){
                            // wholly contained
                            merged.push(b);
                        }else if(b.end >= a.end){
                            a.end = b.end;
                            merged.push(b);
                        }else{
                            console.error('holy fuck');
                        }
                    }
                }
                for(var i=0; i<merged.length;i++){
                    list.splice(list.indexOf(merged[i]), 1);
                }
                if(!has_merged){
                    index++;                    
                }
                if(index === list.length){
                    return list;
                }else{
                    return merge(list, index);                    
                }
            }
            
            merged_masks = merge(merged_masks, 0);
            event_masks.map(function(d){
                d.tooltipUrl = "/calendar/day/?day="+d.start.format('d')+"&month="+ d.start.format('m') +"&year=" + d.start.format('Y');
                return d; 
            });
            
            /* The root panel. */
            var vis = new pv.Panel()
                .width(w)
                .height(h)
                .bottom(20)    

            // Marks representing completed tickets
            vis.add(pv.Dot)
                .data(all_tickets)
                .visible(function(d) d.completed)
                .left(function(d) x(d.updated))
                .top(function(d) y(d.project + ':Milestone-' + d.milestone + '-' + d.owner))
                .size(function(d) d.estimated_time * 10)
                .fillStyle("rgba(10, 10, 10, .2)")
                .strokeStyle("white")
                .event("mouseover", pv.Behavior.qtip(qtipOpts))


            // Holiday column masking
            vis.add(pv.Bar)
                .data(holidays)
                .top(-4)
                .height(h + 30)
                .left(function(d) x(d.start))
                .width(function(d) x(d.end) - x(d.start))
                .fillStyle("#F3FBE9")
                .strokeStyle('#C1E5B6')
                .title(function(d) d.name)
                .cursor("pointer");

            // Weekend column masking
            vis.add(pv.Bar)
                .data(weekends)
                .top(-4)
                .height(h +200)
                .left(function(d) x(d.start))
                .width(function(d) x(d.end) - x(d.start))
                .fillStyle("#f9f9f9")
                .strokeStyle('#e5e5e5')
                .anchor('center').add(pv.Label)
                    .top(75)
                    .textStyle('#ccc')
                    .textAngle(-Math.PI / 2)
                    .text('weekend')

            /* Rule to represent today's date/time */
            vis.add(pv.Rule)
                .data(function() [new Date()])
                .left(function(d) x(d))
                // .top(-5)
                .height(h + 100)
                .strokeStyle("#FFDC76")
                .lineWidth(5)
                
            // Milestone labels
            vis.add(pv.Rule)
                .data(milestones)
                .top(function(d) y(d.project + ':' + d.name))
                .height(function(d){
                    if(this.index === milestones.length - 1){
                        return 1000;
                    }else{
                        var next = milestones[this.index+1];
                        return y(next.project + ':' + next.name) - this.top() - row_height;
                    }
                })
                .left(function(d) x(d.due))
                .strokeStyle("#FF9499")
                .lineWidth(5)
                // Label for milestone
                .anchor('top').add(pv.Dot)
                    // Shim for positioning label
                    .size(0)
                    .anchor('left').add(pv.Label)
                        // actual label
                        .text(function(d) d.project + ' - ' + d.name)
                        .textBaseline('bottom')
                        .textAlign('right')
                        .textStyle("#FF7078")
                        .font("16px sans-serif")
                        .textMargin(5)

            vis.add(pv.Bar)
                .data(all_tickets)
                .visible(function(d) !d.completed)
                .left(function(d) x(d.start))
                .width(function(d) x(d.end) - x(d.start))
                .top(function(d) y(d.project + ':Milestone-' + d.milestone + '-' + d.owner) - 5)
                .height(10)
                .fillStyle(function(d) INCOMPLETE_COLORS(d.id))
                .event("mouseover", pv.Behavior.qtip(qtipOpts))
                .anchor('left').add(pv.Panel)
                    .visible(function(d) d.first)
                    .width(20)
                    .add(pv.Image)
                        .url(function(d) committers[d.owner].gravatar)
                        .left(-10)
                        .top(-10)
                        .width(20)
                        .height(20)
                        .title(function(d) committers[d.owner].name)
                        .event("click", function(d) window.location = '/people/'+committers[d.owner].key+'/')
                        .cursor("pointer");

            // Event stripe mask
            vis.add(pv.Panel)
                .data(merged_masks)
                .top(function(d) y(d.y) - 5)
                .height(10)
                .left(function(d) x(d.start))
                .width(function(d) x(d.end) - x(d.start))
                .overflow('hidden')
                .add(pv.Image)
                    .url('/images/weekend_mask.png')
                    .width(500)
            
            // Vacation Mask
            vis.add(pv.Bar)
                .data(event_masks)
                .visible(function(d) d.type && d.type === 'Vacation')
                .top(function(d) y(d.y) - 5)
                .height(10)
                .left(function(d) x(d.start))
                .width(function(d) x(d.end) - x(d.start))
                .fillStyle("rgba(50, 200, 60, .5)")
                .event("mouseover", pv.Behavior.qtip(qtipOpts))
                .cursor("pointer");
        
            // Meeting Mask
            vis.add(pv.Bar)
                .data(event_masks)
                .visible(function(d) d.type && d.type === 'Meeting')
                .top(function(d) y(d.y) - 5)
                .height(10)
                .left(function(d) x(d.start))
                .width(function(d) x(d.end) - x(d.start))
                .fillStyle("rgba(255, 200, 90, .5)")
                .event("mouseover", pv.Behavior.qtip(qtipOpts))
                .cursor("pointer");
            
            // Other Obligation Mask
            vis.add(pv.Bar)
                .data(event_masks)
                .visible(function(d) d.type && d.type === 'Other')
                .top(function(d) y(d.y) - 5)
                .height(10)
                .left(function(d) x(d.start))
                .width(function(d) x(d.end) - x(d.start))
                .fillStyle("rgba(20, 0, 200, .5)")
                .event("mouseover", pv.Behavior.qtip(qtipOpts))
                .cursor("pointer");

            // Major and minor tics for each weekday
            vis.add(pv.Rule)
                .data(day_ticks)
                .left(function(d) x(d))
                .height(function(d) (d.getDay() === 1 ? 32 : 3))
                .strokeStyle("#6EA2C9")
                // Abbreviated day labels for minor tics
                .anchor("bottom").add(pv.Label)
                    .visible(function(d) d.getDay() != 1)
                    .text(function(d) ['Sun', 'M', 'T', 'W', 'Th', 'F', 'S'][d.getDay()])
                    .font("10px sans-serif")
                    .textStyle("#628D99")
                // Full date labels for major tics (Mondays)
                .anchorTarget().anchor("right").add(pv.Label)
                    .top(25)
                    .textMargin(5)
                    // must be a Monday and not the last day in the timeline
                    .visible(function(d) d.getDay() === 1 && this.index != day_ticks.length - 1)
                    .text(function(d) d.toLocaleString().split('00')[0])
                    .textStyle("#628D99")

            vis.render();
            console.timeEnd('rendering');
        </script>
    {% else %}
        <div class="instance">
            <p><em>No milestones or projects to report on.</em></p>
        </div>
    {% endif %}
{% endblock body %}
{% block aftercontent %}
<script>
    $(document).ready(function(){
        $('div.instructions').hide();
        $('a.instructions').click(function(e){
            e.preventDefault();
            $('div.instructions').toggle();
            return false;
        });
    });
</script>
    <h3><a class="instructions" href="#">How do I use this?</a></h3>
    <div class="instructions">
        <p>This timeline is driven by two sources of information:</p>
        <h4>Project Issue Trackers</h4>
        <p>All tasks are held in the issue tracker and pulled periodically into this app. If the data here looks stale, hit <a class="refresh" href="/projects/update/">refresh data</a> to manually refresh the task lists.</p>
        <p>This tool looks for special labels attached to tickets to guide it's presentation:
            <dl class="labels">
                <dt><code class="label">Milestone-</code></dt>
                <dd>The milestone label sets a ticket to be a part of a milestone. Date can be set on milestones using this app as described <a href="#milestones">below</a></dd>
                <dt><code class="label">Owner-</code></dt>
                <dd>Owner determines who is responsible for the work. If no owner is specified yet a ticket is assigned to a milestone, it will be added to an "Unassigned" user's work list.</dd>
                <dt><code class="label">EstimatedTime-</code></dt>
                <dd>This label specifies the number of hours needed to complete the whole ticket. <em>Avoid changing the hours</em> if a task is partially finished. Instead, use the next label.</dd>
                <dt><code class="label">PercentComplete-</code></dt>
                <dd>Use this to specify that a task is part-way finished. The length of the ticket on the timeline will change to reflect this fact.</dd>
            </dl>
        </p>
        <p>To effectively use this tool, you should use the google project hosting admin interface to add this list of standard labels:
            <pre>
                Phase-1             
                Phase-2             
                Phase-3             
                Phase-4             
                Phase-5             
                Milestone-2.0
                Category-CMSRefactor
                Category-App2App    
                Category-Conversations
                Category-CollaborationFeatures
                Category-TemplatingEnhancements
                Category-MiscUIBacklog
                Category-Deployment 
                Category-Testing    
                EstimatedTime-1      = Should take about 1 hour
                EstimatedTime-2      = Should take about 2 hours
                EstimatedTime-4      = Should take about 4 hours
                EstimatedTime-8      = Should take about 1 day
                EstimatedTime-16     = Should take about 2 days
                EstimatedTime-24     = Should take about 3 days
                EstimatedTime-40     = Should take about 1 week
                EstimatedTime-80     = Should take about 2 weeks
                EstimatedTime-120    = Should take about 3 weeks
                EstimatedTime-160    = Should take about 1 month
                PercentComplete-10   = 10%
                PercentComplete-20   = 20%
                PercentComplete-30   = 30%
                PercentComplete-40   = 40%
                PercentComplete-50   = 50%
                PercentComplete-60   = 60%
                PercentComplete-70   = 70%
                PercentComplete-80   = 80%
                PercentComplete-90   = 90% - Woa!, don't you think you should just finish?
            </pre>
        </p>
        <h4>Milestones, Holidays, Vacations, and Meetings</h4>
        <p>All this information can be specified using the Projects, People, and Calendar menu items.</p>
        <ul>
            <li id="milestones"><strong>Milestones</strong> can be specified on the project page. You can change the date whenever you like and have changes reflected on the timeline. The only requirement is that the name of a milestone exactly match the milestone label in the issue tracker, ie <code class="label">Milestone-2.0</code> or <code class="label">Milestone-1.3</code></li>
            <li><strong>Holidays, Vacations, and Meetings</strong> can be specified on the Calendar. <strong>Holidays</strong> apply to all contributors, while <strong>Vacations and Meetings</strong> only apply to listed people.</li>
        </ul>
    </div>
{% endblock aftercontent %}